{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "header-section",
      "metadata": {
        "id": "header-section"
      },
      "source": [
        "# Multimodal Data Preparation for Melanoma Detection with Axolotl\n",
        "\n",
        "<table align=\"left\">\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://colab.research.google.com/github/GoogleCloudPlatform/kubernetes-engine-samples/blob/main/ai-ml/axolotl-multimodal-finetuning-gemma/data/DataPreparation.ipynb\">\n",
        "      <img src=\"https://cloud.google.com/ml-engine/images/colab-logo-32px.png\" alt=\"Google Colaboratory logo\"><br> Run in Colab\n",
        "    </a>\n",
        "  </td>\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://console.cloud.google.com/vertex-ai/colab/import/https:%2F%2Fraw.githubusercontent.com%2FGoogleCloudPlatform%2Fkubernetes-engine-samples%2Fmain%2Fai-ml%2Faxolotl-multimodal-finetuning-gemma%2Fdata%2FDataPreparation.ipynb\">\n",
        "      <img width=\"32px\" src=\"https://lh3.googleusercontent.com/JmcxdQi-qOpctIvWKgPtrzZdJJK-J3sWE1RsfjZNwshCFgE_9fULcNpuXYTilIR2hjwN\" alt=\"Google Cloud Colab Enterprise logo\"><br> Run in Colab Enterprise\n",
        "    </a>\n",
        "  </td>\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://github.com/GoogleCloudPlatform/kubernetes-engine-samples/blob/main/ai-ml/axolotl-multimodal-finetuning-gemma/data/DataPreparation.ipynb\">\n",
        "      <img src=\"https://cloud.google.com/ml-engine/images/github-logo-32px.png\" alt=\"GitHub logo\"><br> View on GitHub\n",
        "    </a>\n",
        "  </td>\n",
        "  <td style=\"text-align: center\">\n",
        "    <a href=\"https://console.cloud.google.com/vertex-ai/workbench/deploy-notebook?download_url=https://raw.githubusercontent.com/GoogleCloudPlatform/kubernetes-engine-samples/main/ai-ml/axolotl-multimodal-finetuning-gemma/data/DataPreparation.ipynb\">\n",
        "      <img src=\"https://lh3.googleusercontent.com/UiNooY4LUgW_oTvpsNhPpQzsstV5W8F7rYgxgGBD85cWJoLmrOzhVs_ksK_vgx40SHs7jCqkTkCk=e14-rj-sc0xffffff-h130-w32\" alt=\"Vertex AI logo\"><br> Open in Vertex AI Workbench\n",
        "    </a>\n",
        "  </td>\n",
        "</table>"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "overview-section",
      "metadata": {
        "id": "overview-section"
      },
      "source": [
        "## Overview\n",
        "\n",
        "This notebook demonstrates how to prepare the [SIIM-ISIC Melanoma Classification](https://challenge2020.isic-archive.com/) dataset for fine-tuning multimodal AI models using Axolotl on Google Kubernetes Engine (GKE). The process transforms over 33,000 dermoscopic images into the specific format required for multimodal fine-tuning.\n",
        "\n",
        "### What you'll learn\n",
        "\n",
        "- How to efficiently download and process large medical imaging datasets using Google Cloud Storage\n",
        "- How to create stratified train/validation/test splits while maintaining class distribution\n",
        "- How to format multimodal data for Axolotl's chat template format\n",
        "- How to handle class imbalance in medical imaging datasets\n",
        "\n",
        "### Prerequisites\n",
        "\n",
        "- Google Cloud Project with billing enabled\n",
        "- Access to Google Cloud Storage\n",
        "- Python 3.8+ environment\n",
        "- Approximately 40GB of temporary storage for processing\n",
        "\n",
        "### Time to complete\n",
        "\n",
        "30-40 minutes (primarily due to downloading and processing 32GB of images)\n",
        "\n",
        "---"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6860a1ff-8e2f-4841-8332-1ee318c7bed6",
      "metadata": {
        "id": "6860a1ff-8e2f-4841-8332-1ee318c7bed6"
      },
      "source": [
        "## Introduction\n",
        "\n",
        "The SIIM-ISIC dataset contains over 33,000 dermoscopic images of skin lesions with corresponding labels indicating whether each lesion is benign or malignant melanoma. The dataset was released as part of the ISIC 2020 Challenge to help improve melanoma detection algorithms. Each image comes with additional metadata including the patient's age, sex, and the anatomical site of the lesion.\n",
        "\n",
        "Our goal is to transform this raw medical imaging dataset into the specific format required by Axolotl for multimodal fine-tuning. This involves:\n",
        "\n",
        "1. **Downloading and exploring the dataset** - Understanding the data structure and class distribution\n",
        "2. **Splitting the data** - Creating training (80%), validation (10%), and test (10%) sets while maintaining class distribution\n",
        "3. **Processing images and labels** - Converting to Axolotl's chat template format\n",
        "4. **Creating JSONL files** - Generating properly formatted files that maintain image-diagnosis relationships\n",
        "\n",
        "The output will be a training-ready dataset that follows the [chat_template](https://docs.axolotl.ai/docs/multimodal.html#dataset-format) format for fine-tuning Gemma 3 on this melanoma classification task.\n",
        "\n",
        "**⚠️ Note**: This notebook contains medical imagery. The content is intended for educational and research purposes only."
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a1816ee8-3a24-4af3-860e-5cb7b12cf85d",
      "metadata": {
        "id": "a1816ee8-3a24-4af3-860e-5cb7b12cf85d"
      },
      "source": [
        "## Step 1: Install dependencies\n",
        "\n",
        "First, let's install the required packages for data processing and Google Cloud Storage integration:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "f785597a-0976-46d0-b959-33d420588b61",
      "metadata": {
        "tags": [],
        "id": "f785597a-0976-46d0-b959-33d420588b61"
      },
      "outputs": [],
      "source": [
        "# Install required packages\n",
        "!pip install google-cloud-storage numpy==1.26.4 pandas matplotlib seaborn scikit-learn tqdm -q\n",
        "\n",
        "# Import to verify installation\n",
        "import google.cloud.storage\n",
        "print(f\"✅ google-cloud-storage version: {google.cloud.storage.__version__}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b9d359d0-9fc2-44b0-b713-2bfb1da354d4",
      "metadata": {
        "id": "b9d359d0-9fc2-44b0-b713-2bfb1da354d4"
      },
      "source": [
        "## Step 2: Set up your environment\n",
        "\n",
        "Configure your Google Cloud authentication and project settings:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "auth-setup",
      "metadata": {
        "id": "auth-setup"
      },
      "outputs": [],
      "source": [
        "import os\n",
        "import sys\n",
        "\n",
        "# Check if we're running in Colab\n",
        "IN_COLAB = 'google.colab' in sys.modules\n",
        "\n",
        "if IN_COLAB:\n",
        "    from google.colab import auth\n",
        "    auth.authenticate_user()\n",
        "    print(\"✅ Authenticated via Colab\")\n",
        "else:\n",
        "    # For Vertex AI Workbench or local environments\n",
        "    print(\"ℹ️ Using Application Default Credentials\")\n",
        "    print(\"   If not authenticated, run: gcloud auth application-default login\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "project-setup",
      "metadata": {
        "id": "project-setup"
      },
      "outputs": [],
      "source": [
        "# Set your project ID\n",
        "PROJECT_ID = \"YOUR_PROJECT_ID\"  # @param {type:\"string\"}\n",
        "\n",
        "# Set project\n",
        "!gcloud config set project {PROJECT_ID}\n",
        "\n",
        "# Verify project is set\n",
        "!echo \"Current project: $(gcloud config get-value project)\""
      ]
    },
    {
      "cell_type": "markdown",
      "id": "67d0a854-f72c-4a53-bf3d-608b1f8f593c",
      "metadata": {
        "id": "67d0a854-f72c-4a53-bf3d-608b1f8f593c"
      },
      "source": [
        "## Step 3: Import libraries and configure settings\n",
        "\n",
        "Import all necessary libraries and set up configuration parameters:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "eca28e71-1285-4f3a-8028-61e438b303cd",
      "metadata": {
        "tags": [],
        "id": "eca28e71-1285-4f3a-8028-61e438b303cd"
      },
      "outputs": [],
      "source": [
        "import json\n",
        "import os\n",
        "import shutil\n",
        "import tempfile\n",
        "import warnings\n",
        "import zipfile\n",
        "import time\n",
        "from concurrent.futures import ThreadPoolExecutor, as_completed\n",
        "import logging\n",
        "\n",
        "# Third-party imports\n",
        "from google.cloud import storage\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import seaborn as sns\n",
        "from sklearn.model_selection import train_test_split\n",
        "from tqdm.notebook import tqdm\n",
        "\n",
        "# Suppress warnings for cleaner output\n",
        "warnings.filterwarnings(\"ignore\", category=FutureWarning)\n",
        "\n",
        "# Configuration\n",
        "# ⚠️ IMPORTANT: Update these values with your bucket name\n",
        "GCS_BUCKET_NAME = f\"{PROJECT_ID}-melanoma-dataset\"  # @param {type:\"string\"}\n",
        "SOURCE_BUCKET = GCS_BUCKET_NAME\n",
        "DEST_BUCKET = GCS_BUCKET_NAME\n",
        "SOURCE_FOLDER = \"isic-challenge-data.s3.amazonaws.com/2020\"\n",
        "DEST_FOLDER = \"axolotl-data\"\n",
        "\n",
        "print(f\"📁 Using GCS bucket: gs://{GCS_BUCKET_NAME}\")\n",
        "print(f\"📂 Source folder: {SOURCE_FOLDER}\")\n",
        "print(f\"📂 Destination folder: {DEST_FOLDER}\")\n",
        "\n",
        "# Initialize Cloud Storage client\n",
        "client = storage.Client(project=PROJECT_ID)\n",
        "source_bucket = client.bucket(SOURCE_BUCKET)\n",
        "dest_bucket = client.bucket(DEST_BUCKET)\n",
        "\n",
        "# Create temp directories\n",
        "os.makedirs(\"/tmp/train_images\", exist_ok=True)\n",
        "print(\"✅ Temporary directories created\")\n",
        "\n",
        "# Set thread pool parameters for parallel processing\n",
        "MAX_WORKERS = 16  # Adjust based on your system's capabilities\n",
        "print(f\"🔧 Parallel processing with {MAX_WORKERS} workers\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bucket-check",
      "metadata": {
        "id": "bucket-check"
      },
      "source": [
        "### Verify bucket access"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "verify-bucket",
      "metadata": {
        "id": "verify-bucket"
      },
      "outputs": [],
      "source": [
        "# Check if bucket exists and is accessible\n",
        "try:\n",
        "    source_bucket.reload()\n",
        "    print(f\"✅ Successfully connected to bucket: gs://{GCS_BUCKET_NAME}\")\n",
        "    print(f\"   Location: {source_bucket.location}\")\n",
        "    print(f\"   Storage class: {source_bucket.storage_class}\")\n",
        "except Exception as e:\n",
        "    print(f\"❌ Error accessing bucket: {e}\")\n",
        "    print(f\"   Please ensure the bucket exists and you have access permissions\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a5d73efc-ede2-406e-b396-9f3e2267dd93",
      "metadata": {
        "id": "a5d73efc-ede2-406e-b396-9f3e2267dd93"
      },
      "source": [
        "## Step 4: Explore the dataset\n",
        "\n",
        "Let's list and examine the files available in the SIIM-ISIC dataset:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "8abcb1f6-6f46-40ba-af74-162f1254225c",
      "metadata": {
        "tags": [],
        "id": "8abcb1f6-6f46-40ba-af74-162f1254225c"
      },
      "outputs": [],
      "source": [
        "print(\"📋 Listing files in the SIIM-ISIC dataset:\")\n",
        "print(\"=\" * 60)\n",
        "\n",
        "files = list(source_bucket.list_blobs(prefix=SOURCE_FOLDER))\n",
        "for i, file in enumerate(files, 1):\n",
        "    file_size_mb = file.size / (1024 * 1024)\n",
        "    print(f\"{i}. {file.name.split('/')[-1]} ({file_size_mb:.1f} MB)\")\n",
        "\n",
        "print(f\"\\n📊 Total files: {len(files)}\")\n",
        "total_size_gb = sum(f.size for f in files) / (1024 * 1024 * 1024)\n",
        "print(f\"💾 Total size: {total_size_gb:.1f} GB\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "941e6a57-3ead-4d79-a943-918fa508ba0b",
      "metadata": {
        "tags": [],
        "id": "941e6a57-3ead-4d79-a943-918fa508ba0b"
      },
      "source": [
        "## Step 5: Download and visualize dataset metadata\n",
        "\n",
        "Let's download the metadata CSV file and create comprehensive visualizations to understand the dataset characteristics:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "cd19aee2-3ed5-4aa3-a064-fa8c94ff0fa5",
      "metadata": {
        "tags": [],
        "id": "cd19aee2-3ed5-4aa3-a064-fa8c94ff0fa5"
      },
      "outputs": [],
      "source": [
        "# Download metadata CSV and examine structure\n",
        "print(\"📥 Downloading metadata...\")\n",
        "metadata_blob = source_bucket.blob(f\"{SOURCE_FOLDER}/ISIC_2020_Training_GroundTruth_v2.csv\")\n",
        "with tempfile.NamedTemporaryFile() as temp_file:\n",
        "    metadata_blob.download_to_filename(temp_file.name)\n",
        "    train_metadata = pd.read_csv(temp_file.name)\n",
        "\n",
        "    # Print dataset statistics\n",
        "    print(\"\\n📊 Dataset Overview:\")\n",
        "    print(f\"Total samples: {len(train_metadata):,}\")\n",
        "    print(f\"\\nColumns: {', '.join(train_metadata.columns)}\")\n",
        "    print(f\"\\nFirst few rows:\")\n",
        "    display(train_metadata.head())\n",
        "\n",
        "    # Set plot style\n",
        "    sns.set(style=\"whitegrid\")\n",
        "    plt.rcParams.update({'font.size': 12})\n",
        "\n",
        "    # Figure 1: Key Distributions\n",
        "    plt.figure(figsize=(18, 14))\n",
        "\n",
        "    # 1. Anatomical Site Distribution\n",
        "    plt.subplot(2, 2, 1)\n",
        "    site_counts = train_metadata['anatom_site_general_challenge'].value_counts()\n",
        "    ax = plt.barh(site_counts.index, site_counts.values, color=plt.cm.Reds(np.linspace(0.3, 0.7, len(site_counts))))\n",
        "    plt.title('Anatomical Site Distribution', fontsize=16, pad=20)\n",
        "    plt.xlabel('Count', fontsize=14)\n",
        "\n",
        "    # Add count labels to bars\n",
        "    for i, v in enumerate(site_counts.values):\n",
        "        plt.text(v + 100, i, f'{v:,}', va='center')\n",
        "\n",
        "    # 2. Age Distribution by Malignancy\n",
        "    plt.subplot(2, 2, 2)\n",
        "    ax = sns.boxplot(x='benign_malignant', y='age_approx', data=train_metadata,\n",
        "                     palette={'benign': 'lightgreen', 'malignant': 'salmon'})\n",
        "    plt.title('Age Distribution by Malignancy', fontsize=16, pad=20)\n",
        "    plt.xlabel('Diagnosis Type', fontsize=14)\n",
        "    plt.ylabel('Age', fontsize=14)\n",
        "\n",
        "    # 3. Sex Distribution\n",
        "    plt.subplot(2, 2, 3)\n",
        "    sex_counts = train_metadata['sex'].value_counts()\n",
        "    ax = plt.bar(sex_counts.index, sex_counts.values, color=plt.cm.Blues(np.linspace(0.4, 0.7, len(sex_counts))))\n",
        "    plt.title('Sex Distribution', fontsize=16, pad=20)\n",
        "    plt.ylabel('Count', fontsize=14)\n",
        "\n",
        "    # Add count labels to bars\n",
        "    for i, v in enumerate(sex_counts.values):\n",
        "        plt.text(i, v + 100, f'{v:,}', ha='center')\n",
        "\n",
        "    # 4. Benign vs Malignant Distribution\n",
        "    plt.subplot(2, 2, 4)\n",
        "    diagnosis_counts = train_metadata['benign_malignant'].value_counts()\n",
        "    plt.pie(diagnosis_counts, labels=diagnosis_counts.index, autopct='%1.1f%%',\n",
        "            colors=['lightgreen', 'salmon'], startangle=90, explode=(0, 0.1))\n",
        "    plt.title('Benign vs Malignant Distribution', fontsize=16, pad=20)\n",
        "    plt.axis('equal')\n",
        "\n",
        "    # Add text annotations with counts\n",
        "    plt.annotate(f\"Benign: {diagnosis_counts['benign']:,}\", xy=(-1.2, -0.8), fontsize=12)\n",
        "    plt.annotate(f\"Malignant: {diagnosis_counts['malignant']:,}\", xy=(0.8, -0.8), fontsize=12)\n",
        "\n",
        "    plt.tight_layout(pad=3.0)\n",
        "    plt.show()\n",
        "\n",
        "    # Figure 2: Relationships between variables\n",
        "    plt.figure(figsize=(18, 14))\n",
        "\n",
        "    # 1. Malignancy by Sex\n",
        "    plt.subplot(2, 2, 1)\n",
        "    sex_malig = pd.crosstab(train_metadata['sex'], train_metadata['benign_malignant'])\n",
        "    ax = sex_malig.plot(kind='bar', color=['lightgreen', 'salmon'], ax=plt.gca())\n",
        "    plt.title('Malignancy by Sex', fontsize=16, pad=20)\n",
        "    plt.ylabel('Count', fontsize=14)\n",
        "    plt.xlabel('Sex', fontsize=14)\n",
        "    plt.xticks(rotation=0)\n",
        "    plt.legend(title='Diagnosis')\n",
        "\n",
        "    # Add text labels\n",
        "    for p in ax.patches:\n",
        "        ax.annotate(f'{int(p.get_height()):,}',\n",
        "                   (p.get_x() + p.get_width() / 2., p.get_height()),\n",
        "                   ha = 'center', va = 'bottom')\n",
        "\n",
        "    # 2. Malignancy Rate by Sex\n",
        "    plt.subplot(2, 2, 2)\n",
        "    sex_malig_rate = pd.crosstab(train_metadata['sex'],\n",
        "                               train_metadata['benign_malignant'],\n",
        "                               normalize='index') * 100\n",
        "    ax = sex_malig_rate.plot(kind='bar', color=['lightgreen', 'salmon'], ax=plt.gca())\n",
        "    plt.title('Malignancy Rate by Sex (%)', fontsize=16, pad=20)\n",
        "    plt.ylabel('Percentage', fontsize=14)\n",
        "    plt.xlabel('Sex', fontsize=14)\n",
        "    plt.xticks(rotation=0)\n",
        "    plt.legend(title='Diagnosis')\n",
        "\n",
        "    # Add text labels\n",
        "    for p in ax.patches:\n",
        "        ax.annotate(f\"{p.get_height():.1f}%\",\n",
        "                   (p.get_x() + p.get_width() / 2., p.get_height() + 1),\n",
        "                   ha = 'center')\n",
        "\n",
        "    # 3. Malignancy Rate by Anatomical Site\n",
        "    plt.subplot(2, 2, 3)\n",
        "    # Filter out any NaN values and ensure proper calculation\n",
        "    site_data = train_metadata.dropna(subset=['anatom_site_general_challenge'])\n",
        "    site_malig = pd.crosstab(site_data['anatom_site_general_challenge'],\n",
        "                           site_data['benign_malignant'],\n",
        "                           normalize='index') * 100\n",
        "    ax = site_malig.plot(kind='barh', color=['lightgreen', 'salmon'], ax=plt.gca())\n",
        "    plt.title('Malignancy Rate by Anatomical Site (%)', fontsize=16, pad=20)\n",
        "    plt.xlabel('Percentage', fontsize=14)\n",
        "    plt.ylabel('Anatomical Site', fontsize=14)\n",
        "    plt.legend(title='Diagnosis')\n",
        "\n",
        "    # Add percentage labels for malignant cases\n",
        "    for i, v in enumerate(site_malig['malignant']):\n",
        "        plt.text(v + 0.5, i, f\"{v:.1f}%\", va='center')\n",
        "\n",
        "    # 4. Missing Values Visualization\n",
        "    plt.subplot(2, 2, 4)\n",
        "    missing_values = train_metadata.isnull().sum().sort_values(ascending=False)\n",
        "    # Only show columns with missing values\n",
        "    missing_values = missing_values[missing_values > 0]\n",
        "    if len(missing_values) > 0:\n",
        "        ax = plt.barh(missing_values.index, missing_values.values,\n",
        "                     color=plt.cm.Greys(np.linspace(0.3, 0.7, len(missing_values))))\n",
        "        plt.title('Missing Values per Column', fontsize=16, pad=20)\n",
        "        plt.xlabel('Count', fontsize=14)\n",
        "\n",
        "        # Add count labels to bars\n",
        "        for i, v in enumerate(missing_values.values):\n",
        "            plt.text(v + 5, i, f'{v:,}', va='center')\n",
        "    else:\n",
        "        plt.text(0.5, 0.5, \"No missing values!\", ha='center', va='center', fontsize=14)\n",
        "        plt.title('Missing Values Check', fontsize=16, pad=20)\n",
        "        plt.axis('off')\n",
        "\n",
        "    plt.tight_layout(pad=3.0)\n",
        "    plt.show()\n",
        "\n",
        "    # Figure 3: Additional visualizations\n",
        "    plt.figure(figsize=(18, 8))\n",
        "\n",
        "    # 1. Age distribution\n",
        "    plt.subplot(1, 2, 1)\n",
        "    ax = sns.histplot(train_metadata['age_approx'].dropna(), bins=15, kde=True)\n",
        "    plt.title('Age Distribution', fontsize=16, pad=20)\n",
        "    plt.xlabel('Age', fontsize=14)\n",
        "    plt.ylabel('Count', fontsize=14)\n",
        "\n",
        "    # Add statistics annotation\n",
        "    age_mean = train_metadata['age_approx'].mean()\n",
        "    age_median = train_metadata['age_approx'].median()\n",
        "    plt.annotate(f\"Mean: {age_mean:.1f}\\nMedian: {age_median:.1f}\",\n",
        "                xy=(0.05, 0.95), xycoords='axes fraction',\n",
        "                bbox=dict(boxstyle=\"round,pad=0.3\", fc=\"white\", ec=\"gray\", alpha=0.8))\n",
        "\n",
        "    # 2. Class imbalance (target variable)\n",
        "    plt.subplot(1, 2, 2)\n",
        "    target_counts = train_metadata['target'].value_counts()\n",
        "    ax = plt.bar(['Non-Melanoma (0)', 'Melanoma (1)'], target_counts.values,\n",
        "               color=['lightgreen', 'salmon'])\n",
        "    plt.text(0, target_counts[0]/2, f\"{target_counts[0]:,} samples\",\n",
        "           ha='center', va='center', color='black', fontweight='bold')\n",
        "    plt.text(1, target_counts[1]/2, f\"{target_counts[1]:,} samples\",\n",
        "           ha='center', va='center', color='black', fontweight='bold')\n",
        "    plt.title('Class Distribution (Target Variable)', fontsize=16, pad=20)\n",
        "    plt.ylabel('Count', fontsize=14)\n",
        "\n",
        "    # Add imbalance ratio annotation\n",
        "    imbalance_ratio = target_counts[0] / target_counts[1]\n",
        "    plt.figtext(0.5, 0.01, f\"Class imbalance ratio: {imbalance_ratio:.1f}:1 (Non-melanoma:Melanoma)\",\n",
        "              ha='center', fontsize=14, bbox={\"facecolor\":\"orange\", \"alpha\":0.2, \"pad\":5})\n",
        "\n",
        "    plt.tight_layout(pad=3.0)\n",
        "    plt.show()\n",
        "\n",
        "    # Print additional textual information\n",
        "    print(\"\\n📊 Diagnosis distribution:\")\n",
        "    print(train_metadata['diagnosis'].value_counts())\n",
        "\n",
        "    print(\"\\n📊 Benign/Malignant distribution:\")\n",
        "    print(train_metadata['benign_malignant'].value_counts())\n",
        "\n",
        "    # Check for missing values\n",
        "    print(\"\\n⚠️ Missing values per column:\")\n",
        "    missing_summary = train_metadata.isnull().sum()\n",
        "    missing_summary = missing_summary[missing_summary > 0]\n",
        "    if len(missing_summary) > 0:\n",
        "        print(missing_summary)\n",
        "    else:\n",
        "        print(\"No missing values found!\")\n",
        "\n",
        "    # Class imbalance ratio\n",
        "    imbalance_ratio = len(train_metadata[train_metadata['target'] == 0]) / len(train_metadata[train_metadata['target'] == 1])\n",
        "    print(f\"\\n⚖️ Class imbalance ratio (Benign:Malignant): {imbalance_ratio:.2f}:1\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "397aecdf-71cd-479b-aa93-72b1cc2cb9e0",
      "metadata": {
        "id": "397aecdf-71cd-479b-aa93-72b1cc2cb9e0"
      },
      "source": [
        "## Step 6: Download and extract image data\n",
        "\n",
        "Now let's download the actual dermoscopic images. This is the most time-consuming step due to the large file size:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "37501a34-5738-49fa-88cd-e61b17473083",
      "metadata": {
        "id": "37501a34-5738-49fa-88cd-e61b17473083"
      },
      "outputs": [],
      "source": [
        "print(\"📥 Downloading training images ZIP (this may take 10-15 minutes)...\")\n",
        "train_zip_blob = source_bucket.blob(f\"{SOURCE_FOLDER}/ISIC_2020_Training_JPEG.zip\")\n",
        "\n",
        "# Show download progress\n",
        "with tempfile.NamedTemporaryFile(suffix=\".zip\") as temp_zip:\n",
        "    # Download with progress tracking\n",
        "    start_time = time.time()\n",
        "    train_zip_blob.download_to_filename(temp_zip.name)\n",
        "    download_time = time.time() - start_time\n",
        "\n",
        "    print(f\"✅ Download completed in {download_time/60:.1f} minutes\")\n",
        "\n",
        "    print(\"📦 Extracting training images...\")\n",
        "    extract_start = time.time()\n",
        "\n",
        "    with zipfile.ZipFile(temp_zip.name, 'r') as zip_ref:\n",
        "        # Get total number of files for progress tracking\n",
        "        file_list = zip_ref.namelist()\n",
        "        total_files = len(file_list)\n",
        "\n",
        "        # Extract with progress bar\n",
        "        for file in tqdm(file_list, desc=\"Extracting files\"):\n",
        "            zip_ref.extract(file, \"/tmp/train_images\")\n",
        "\n",
        "    extract_time = time.time() - extract_start\n",
        "    print(f\"✅ Extraction completed in {extract_time/60:.1f} minutes\")\n",
        "\n",
        "# Count the number of image files extracted\n",
        "image_files = [f for f in os.listdir(\"/tmp/train_images/train\") if f.endswith('.jpg')]\n",
        "image_count = len(image_files)\n",
        "print(f\"\\n📸 Extracted {image_count:,} images to temporary directory\")\n",
        "print(f\"💾 Total size: {sum(os.path.getsize(f'/tmp/train_images/train/{f}') for f in image_files) / (1024**3):.1f} GB\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e7283d21-2805-4433-af07-2773524915f4",
      "metadata": {
        "id": "e7283d21-2805-4433-af07-2773524915f4"
      },
      "source": [
        "## Step 7: Create stratified train/validation/test splits\n",
        "\n",
        "We'll split the data into training (80%), validation (10%), and test (10%) sets while maintaining the class distribution:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1f3b8dc5-ea7f-40fb-a279-6a8555204960",
      "metadata": {
        "tags": [],
        "id": "1f3b8dc5-ea7f-40fb-a279-6a8555204960"
      },
      "outputs": [],
      "source": [
        "print(\"🔀 Creating stratified train/validation/test splits...\\n\")\n",
        "\n",
        "# First split: 80% train, 20% temp\n",
        "train_df, temp_df = train_test_split(\n",
        "    train_metadata,\n",
        "    test_size=0.2,\n",
        "    random_state=42,\n",
        "    stratify=train_metadata['target']\n",
        ")\n",
        "\n",
        "# Second split: Split the temp set into validation and test (50/50)\n",
        "val_df, test_df = train_test_split(\n",
        "    temp_df,\n",
        "    test_size=0.5,\n",
        "    random_state=42,\n",
        "    stratify=temp_df['target']\n",
        ")\n",
        "\n",
        "print(\"📊 Dataset splits:\")\n",
        "print(f\"  • Training:   {len(train_df):,} examples ({len(train_df)/len(train_metadata)*100:.1f}%)\")\n",
        "print(f\"  • Validation: {len(val_df):,} examples ({len(val_df)/len(train_metadata)*100:.1f}%)\")\n",
        "print(f\"  • Test:       {len(test_df):,} examples ({len(test_df)/len(train_metadata)*100:.1f}%)\")\n",
        "\n",
        "# Verify class distribution in each split\n",
        "print(\"\\n⚖️ Class distribution (target=1 is melanoma):\")\n",
        "for split_name, split_df in [(\"Training\", train_df), (\"Validation\", val_df), (\"Test\", test_df)]:\n",
        "    counts = split_df['target'].value_counts().sort_index()\n",
        "    melanoma_pct = counts[1] / len(split_df) * 100\n",
        "    print(f\"\\n{split_name} set:\")\n",
        "    print(f\"  • Benign (0):    {counts[0]:,} ({counts[0]/len(split_df)*100:.1f}%)\")\n",
        "    print(f\"  • Melanoma (1):  {counts[1]:,} ({melanoma_pct:.1f}%)\")\n",
        "\n",
        "# Visualize the splits\n",
        "plt.figure(figsize=(12, 6))\n",
        "\n",
        "splits_data = {\n",
        "    'Training': train_df['target'].value_counts(),\n",
        "    'Validation': val_df['target'].value_counts(),\n",
        "    'Test': test_df['target'].value_counts()\n",
        "}\n",
        "\n",
        "x = np.arange(2)\n",
        "width = 0.25\n",
        "\n",
        "for i, (split_name, counts) in enumerate(splits_data.items()):\n",
        "    plt.bar(x + i*width, counts.values, width, label=split_name)\n",
        "\n",
        "plt.xlabel('Class')\n",
        "plt.ylabel('Count')\n",
        "plt.title('Class Distribution Across Splits')\n",
        "plt.xticks(x + width, ['Benign (0)', 'Melanoma (1)'])\n",
        "plt.legend()\n",
        "plt.grid(True, alpha=0.3)\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "26c6ed33-8e6c-498c-834a-1a3d7941e1b6",
      "metadata": {
        "id": "26c6ed33-8e6c-498c-834a-1a3d7941e1b6"
      },
      "source": [
        "## Step 8: Define data processing functions\n",
        "\n",
        "Now we'll create functions to process the data into Axolotl's chat template format:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "039ed9a6-3551-42f1-8d86-87ab7c319d59",
      "metadata": {
        "tags": [],
        "id": "039ed9a6-3551-42f1-8d86-87ab7c319d59"
      },
      "outputs": [],
      "source": [
        "def process_single_example(row, split_name, dataset_type=None, include_metadata=False):\n",
        "    \"\"\"\n",
        "    Process a single example from the dataset into Axolotl's chat template format.\n",
        "\n",
        "    Args:\n",
        "        row: Pandas DataFrame row with the data for this example\n",
        "        split_name: String indicating the split type (train, val, test)\n",
        "        dataset_type: String indicating the dataset type folder prefix (optional)\n",
        "        include_metadata: Boolean indicating whether to include patient metadata\n",
        "\n",
        "    Returns:\n",
        "        Processed example in Axolotl's multimodal format\n",
        "    \"\"\"\n",
        "    image_name = f\"{row['image_name']}.jpg\"\n",
        "\n",
        "    # For GCS FUSE mounting, paths should not include the bucket name (since the bucket becomes the mount point)\n",
        "    if dataset_type:\n",
        "        # Path used in the Axolotl input (for GCS FUSE mounting)\n",
        "        image_path = f\"/mnt/gcs/processed_images/{dataset_type}/{split_name}/{image_name}\"\n",
        "        # Path used for uploading to GCS (includes the bucket)\n",
        "        upload_path = f\"processed_images/{dataset_type}/{split_name}/{image_name}\"\n",
        "    else:\n",
        "        # Path used in the Axolotl input (for GCS FUSE mounting)\n",
        "        image_path = f\"/mnt/gcs/processed_images/{split_name}/{image_name}\"\n",
        "        # Path used for uploading to GCS (includes the bucket)\n",
        "        upload_path = f\"processed_images/{split_name}/{image_name}\"\n",
        "\n",
        "    # Upload image to processed_images folder\n",
        "    image_local_path = f\"/tmp/train_images/train/{image_name}\"\n",
        "    if os.path.exists(image_local_path):\n",
        "        dest_blob = dest_bucket.blob(upload_path)\n",
        "        try:\n",
        "            dest_blob.upload_from_filename(image_local_path)\n",
        "        except Exception as e:\n",
        "            print(f\"Failed to upload {image_name}: {str(e)}\")\n",
        "\n",
        "    is_malignant = row['target'] == 1\n",
        "\n",
        "    # Prepare the user prompt based on whether we include metadata\n",
        "    if include_metadata:\n",
        "        # Get metadata values (with error handling for missing or NaN values)\n",
        "        sex = row['sex'] if pd.notna(row['sex']) else \"unknown\"\n",
        "        age = row['age_approx'] if pd.notna(row['age_approx']) else \"unknown\"\n",
        "        site = row['anatom_site_general_challenge'] if pd.notna(row['anatom_site_general_challenge']) else \"unknown\"\n",
        "\n",
        "        user_text = f\"This is a skin lesion from a {sex} patient, age {age}, located on the {site}. Does this appear to be malignant melanoma?\"\n",
        "    else:\n",
        "        user_text = \"Does this skin lesion appear to be malignant melanoma?\"\n",
        "\n",
        "    # Create the example in Axolotl's multimodal format\n",
        "    example = {\n",
        "        \"messages\": [\n",
        "            {\n",
        "                \"role\": \"system\",\n",
        "                \"content\": [\n",
        "                    {\"type\": \"text\", \"text\": \"You are a dermatology assistant that helps identify potential melanoma from skin lesion images.\"}\n",
        "                ]\n",
        "            },\n",
        "            {\n",
        "                \"role\": \"user\",\n",
        "                \"content\": [\n",
        "                    {\"type\": \"image\", \"path\": image_path},\n",
        "                    {\"type\": \"text\", \"text\": user_text}\n",
        "                ]\n",
        "            },\n",
        "            {\n",
        "                \"role\": \"assistant\",\n",
        "                \"content\": [\n",
        "                    {\"type\": \"text\", \"text\": \"Yes, this appears to be malignant melanoma.\" if is_malignant else\n",
        "                                            \"No, this does not appear to be malignant melanoma.\"}\n",
        "                ]\n",
        "            }\n",
        "        ]\n",
        "    }\n",
        "\n",
        "    return example\n",
        "\n",
        "def process_dataset_split(df, split_name, dataset_type=None, include_metadata=False):\n",
        "    \"\"\"\n",
        "    Process a dataset split (train, validation, or test) and create examples.\n",
        "    Uses parallel processing for efficiency.\n",
        "\n",
        "    Args:\n",
        "        df: Pandas DataFrame with the data for this split\n",
        "        split_name: String indicating the split type (train, val, test)\n",
        "        dataset_type: String indicating the dataset type folder prefix (optional)\n",
        "        include_metadata: Boolean indicating whether to include metadata\n",
        "\n",
        "    Returns:\n",
        "        List of examples in Axolotl's multimodal format\n",
        "    \"\"\"\n",
        "    print(f\"\\n🔄 Processing {len(df):,} examples for {split_name} split\")\n",
        "    examples = []\n",
        "\n",
        "    # Process examples in parallel\n",
        "    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:\n",
        "        # Create a dictionary of futures to their original positions to maintain order\n",
        "        future_to_idx = {\n",
        "            executor.submit(\n",
        "                process_single_example,\n",
        "                row,\n",
        "                split_name,\n",
        "                dataset_type,\n",
        "                include_metadata\n",
        "            ): i for i, (_, row) in enumerate(df.iterrows())\n",
        "        }\n",
        "\n",
        "        # Track progress with tqdm\n",
        "        with tqdm(total=len(df), desc=f\"Processing {split_name} split\") as pbar:\n",
        "            for future in as_completed(future_to_idx):\n",
        "                try:\n",
        "                    example = future.result()\n",
        "                    examples.append(example)\n",
        "                except Exception as e:\n",
        "                    print(f\"Error processing example: {str(e)}\")\n",
        "                pbar.update(1)\n",
        "\n",
        "    return examples\n",
        "\n",
        "def create_and_save_dataset(dataset_type=None, include_metadata=False):\n",
        "    \"\"\"\n",
        "    Create and save a complete dataset (train, validation, test) in Axolotl format.\n",
        "\n",
        "    Args:\n",
        "        dataset_type: String name for the dataset type folder prefix (optional)\n",
        "        include_metadata: Boolean indicating whether to include metadata\n",
        "\n",
        "    Returns:\n",
        "        Tuple of (train_count, val_count, test_count)\n",
        "    \"\"\"\n",
        "    start_time = time.time()\n",
        "\n",
        "    # Log dataset creation start\n",
        "    if dataset_type:\n",
        "        print(f\"\\n🚀 Starting to process {dataset_type} dataset...\")\n",
        "    else:\n",
        "        print(\"\\n🚀 Starting to process dataset...\")\n",
        "\n",
        "    # Create the datasets\n",
        "    train_examples = process_dataset_split(train_df, \"train\", dataset_type, include_metadata)\n",
        "    val_examples = process_dataset_split(val_df, \"val\", dataset_type, include_metadata)\n",
        "    test_examples = process_dataset_split(test_df, \"test\", dataset_type, include_metadata)\n",
        "\n",
        "    # Determine the folder path based on whether dataset_type is provided\n",
        "    if dataset_type:\n",
        "        print(f\"\\n💾 Writing {dataset_type} JSONL files...\")\n",
        "        folder_prefix = f\"{DEST_FOLDER}/{dataset_type}\"\n",
        "    else:\n",
        "        print(\"\\n💾 Writing JSONL files...\")\n",
        "        folder_prefix = DEST_FOLDER\n",
        "\n",
        "    # Write to JSONL files\n",
        "    # Training set\n",
        "    print(f\"  • Writing training set ({len(train_examples):,} examples)...\")\n",
        "    train_output_file = f\"{folder_prefix}/siim_isic_train.jsonl\"\n",
        "    train_blob = dest_bucket.blob(train_output_file)\n",
        "    with train_blob.open(\"w\") as f:\n",
        "        for example in train_examples:\n",
        "            f.write(json.dumps(example) + \"\\n\")\n",
        "\n",
        "    # Validation set\n",
        "    print(f\"  • Writing validation set ({len(val_examples):,} examples)...\")\n",
        "    val_output_file = f\"{folder_prefix}/siim_isic_val.jsonl\"\n",
        "    val_blob = dest_bucket.blob(val_output_file)\n",
        "    with val_blob.open(\"w\") as f:\n",
        "        for example in val_examples:\n",
        "            f.write(json.dumps(example) + \"\\n\")\n",
        "\n",
        "    # Test set\n",
        "    print(f\"  • Writing test set ({len(test_examples):,} examples)...\")\n",
        "    test_output_file = f\"{folder_prefix}/siim_isic_test.jsonl\"\n",
        "    test_blob = dest_bucket.blob(test_output_file)\n",
        "    with test_blob.open(\"w\") as f:\n",
        "        for example in test_examples:\n",
        "            f.write(json.dumps(example) + \"\\n\")\n",
        "\n",
        "    # Calculate and log total processing time\n",
        "    total_time = time.time() - start_time\n",
        "    hours, remainder = divmod(total_time, 3600)\n",
        "    minutes, seconds = divmod(remainder, 60)\n",
        "    print(f\"\\n⏱️ Dataset creation completed in {int(hours)}h {int(minutes)}m {int(seconds)}s\")\n",
        "\n",
        "    return len(train_examples), len(val_examples), len(test_examples)\n",
        "\n",
        "print(\"✅ Data processing functions defined successfully\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "53fec893-d9f0-43a7-b010-9d86f03ca8d9",
      "metadata": {
        "id": "53fec893-d9f0-43a7-b010-9d86f03ca8d9"
      },
      "source": [
        "## Step 9: Create the multimodal dataset\n",
        "\n",
        "Now let's process all the images and create the dataset in Axolotl's format. This step will:\n",
        "1. Upload images to organized folders in GCS\n",
        "2. Create JSONL files with proper chat template formatting"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ef663987-2fee-43e4-a76f-c2b740e646bb",
      "metadata": {
        "tags": [],
        "id": "ef663987-2fee-43e4-a76f-c2b740e646bb"
      },
      "outputs": [],
      "source": [
        "# Create the dataset (without patient metadata)\n",
        "print(\"🏁 Starting dataset creation process...\")\n",
        "print(\"This will upload images to GCS and create JSONL files.\")\n",
        "print(\"Expected time: 15-20 minutes depending on your connection speed.\\n\")\n",
        "\n",
        "train_count, val_count, test_count = create_and_save_dataset(include_metadata=False)\n",
        "\n",
        "print(\"\\n✅ Dataset created successfully!\")\n",
        "print(\"=\" * 50)\n",
        "print(f\"📊 Final dataset statistics:\")\n",
        "print(f\"  • Training examples:   {train_count:,}\")\n",
        "print(f\"  • Validation examples: {val_count:,}\")\n",
        "print(f\"  • Test examples:       {test_count:,}\")\n",
        "print(f\"  • Total examples:      {train_count + val_count + test_count:,}\")\n",
        "\n",
        "# Print GCS paths for reference\n",
        "print(f\"\\n📂 Dataset files created in GCS:\")\n",
        "print(f\"  • gs://{GCS_BUCKET_NAME}/{DEST_FOLDER}/siim_isic_train.jsonl\")\n",
        "print(f\"  • gs://{GCS_BUCKET_NAME}/{DEST_FOLDER}/siim_isic_val.jsonl\")\n",
        "print(f\"  • gs://{GCS_BUCKET_NAME}/{DEST_FOLDER}/siim_isic_test.jsonl\")\n",
        "print(f\"\\n📸 Images uploaded to:\")\n",
        "print(f\"  • gs://{GCS_BUCKET_NAME}/processed_images/train/\")\n",
        "print(f\"  • gs://{GCS_BUCKET_NAME}/processed_images/val/\")\n",
        "print(f\"  • gs://{GCS_BUCKET_NAME}/processed_images/test/\")\n",
        "\n",
        "# Note about metadata-enhanced dataset\n",
        "print(\"\\n💡 Note: You can also create a metadata-enhanced dataset by uncommenting the line below:\")\n",
        "print(\"# metadata_train_count, metadata_val_count, metadata_test_count = create_and_save_dataset('metadata', include_metadata=True)\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "verify-dataset",
      "metadata": {
        "id": "verify-dataset"
      },
      "source": [
        "## Step 10: Verify dataset creation\n",
        "\n",
        "Let's verify that our dataset was created correctly:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "verify-creation",
      "metadata": {
        "id": "verify-creation"
      },
      "outputs": [],
      "source": [
        "print(\"🔍 Verifying dataset creation...\\n\")\n",
        "\n",
        "# Check JSONL files\n",
        "print(\"📄 JSONL files:\")\n",
        "for split in ['train', 'val', 'test']:\n",
        "    blob_path = f\"{DEST_FOLDER}/siim_isic_{split}.jsonl\"\n",
        "    blob = dest_bucket.blob(blob_path)\n",
        "    if blob.exists():\n",
        "        blob.reload()\n",
        "        size_mb = blob.size / (1024 * 1024)\n",
        "        print(f\"  ✅ {blob_path} ({size_mb:.1f} MB)\")\n",
        "    else:\n",
        "        print(f\"  ❌ {blob_path} NOT FOUND\")\n",
        "\n",
        "# Sample a few examples from the training set\n",
        "print(\"\\n📝 Sample training examples:\")\n",
        "train_blob = dest_bucket.blob(f\"{DEST_FOLDER}/siim_isic_train.jsonl\")\n",
        "with train_blob.open(\"r\") as f:\n",
        "    for i, line in enumerate(f):\n",
        "        if i >= 2:  # Show first 2 examples\n",
        "            break\n",
        "        example = json.loads(line)\n",
        "        print(f\"\\nExample {i+1}:\")\n",
        "        print(f\"  System: {example['messages'][0]['content'][0]['text'][:50]}...\")\n",
        "        print(f\"  User text: {example['messages'][1]['content'][1]['text']}\")\n",
        "        print(f\"  Image path: {example['messages'][1]['content'][0]['path']}\")\n",
        "        print(f\"  Assistant: {example['messages'][2]['content'][0]['text']}\")\n",
        "\n",
        "# Check image organization\n",
        "print(\"\\n📸 Checking image organization:\")\n",
        "for split in ['train', 'val', 'test']:\n",
        "    prefix = f\"processed_images/{split}/\"\n",
        "    blobs = list(dest_bucket.list_blobs(prefix=prefix, max_results=5))\n",
        "    count = sum(1 for _ in dest_bucket.list_blobs(prefix=prefix))\n",
        "    print(f\"  • {split}: {count:,} images\")\n",
        "\n",
        "print(\"\\n✅ Dataset verification complete!\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d1c63dec-7608-4683-a87f-880baf6ce153",
      "metadata": {
        "id": "d1c63dec-7608-4683-a87f-880baf6ce153"
      },
      "source": [
        "## Step 11: Clean up temporary files\n",
        "\n",
        "Finally, let's clean up the temporary files to free up disk space:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "1b168d07-b0ee-4fbe-8baf-a058c6667e83",
      "metadata": {
        "tags": [],
        "id": "1b168d07-b0ee-4fbe-8baf-a058c6667e83"
      },
      "outputs": [],
      "source": [
        "# Clean up temp directories\n",
        "print(\"🧹 Cleaning up temporary files...\")\n",
        "shutil.rmtree(\"/tmp/train_images\", ignore_errors=True)\n",
        "print(\"✅ Temporary files removed\")\n",
        "\n",
        "print(\"\\n\" + \"=\" * 60)\n",
        "print(\"🎉 PROCESSING COMPLETE!\")\n",
        "print(\"=\" * 60)\n",
        "print(\"\\n📊 Final Dataset Summary:\")\n",
        "print(f\"  • Total examples: {train_count + val_count + test_count:,}\")\n",
        "print(f\"    - Training:   {train_count:,} ({train_count/(train_count+val_count+test_count)*100:.1f}%)\")\n",
        "print(f\"    - Validation: {val_count:,} ({val_count/(train_count+val_count+test_count)*100:.1f}%)\")\n",
        "print(f\"    - Test:       {test_count:,} ({test_count/(train_count+val_count+test_count)*100:.1f}%)\")\n",
        "print(f\"\\n📁 Data location: gs://{GCS_BUCKET_NAME}/{DEST_FOLDER}/\")\n",
        "print(f\"\\n🚀 Next steps:\")\n",
        "print(f\"  1. Update your gemma3-melanoma.yaml configuration file\")\n",
        "print(f\"  2. Deploy the training job using: ./scripts/deploy-training.sh\")\n",
        "print(f\"  3. Monitor training progress with TensorBoard\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "summary-section",
      "metadata": {
        "id": "summary-section"
      },
      "source": [
        "## Summary\n",
        "\n",
        "In this notebook, we successfully:\n",
        "\n",
        "1. ✅ Downloaded and explored the SIIM-ISIC Melanoma dataset\n",
        "2. ✅ Created stratified train/validation/test splits (80/10/10)\n",
        "3. ✅ Processed 33,000+ dermoscopic images into Axolotl's chat template format\n",
        "4. ✅ Uploaded organized images to Google Cloud Storage\n",
        "5. ✅ Generated JSONL files ready for multimodal fine-tuning\n",
        "\n",
        "The dataset is now ready to be used for fine-tuning Gemma 3 with Axolotl on GKE. The multimodal format allows the model to learn from both visual features and structured prompts, enabling it to make clinically relevant predictions.\n",
        "\n",
        "### Key takeaways\n",
        "\n",
        "- **Class imbalance**: The dataset has approximately 58:1 ratio of benign to malignant cases, reflecting real-world distribution\n",
        "- **Scalable processing**: Parallel processing reduced preparation time from hours to minutes\n",
        "- **Cloud-native approach**: Direct integration with GCS enables seamless training on GKE\n",
        "- **Flexible format**: The chat template format supports both with and without patient metadata\n",
        "\n",
        "### What's next?\n",
        "\n",
        "Check out the [main repository](https://github.com/ayoisio/gke-multimodal-fine-tune-gemma-3-axolotl) for:\n",
        "- Setting up GKE cluster with GPU support\n",
        "- Deploying the Axolotl training job\n",
        "- Evaluating model performance\n",
        "- Comparing with baseline models\n",
        "\n",
        "---\n",
        "\n",
        "**Remember**: This notebook is for educational and research purposes only. Any models trained on this data should not be used for actual medical diagnosis without proper validation and regulatory approval."
      ]
    }
  ],
  "metadata": {
    "environment": {
      "kernel": "conda-base-py",
      "name": "workbench-notebooks.m129",
      "type": "gcloud",
      "uri": "us-docker.pkg.dev/deeplearning-platform-release/gcr.io/workbench-notebooks:m129"
    },
    "kernelspec": {
      "display_name": "Python 3 (ipykernel) (Local)",
      "language": "python",
      "name": "conda-base-py"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.10.16"
    },
    "colab": {
      "provenance": []
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}